安卓逆向:这是一篇逆向基础函数在ARM32中的刨根问底。
ARM32中函数需要关注的点有哪些?
1. 怎么去识别ARM的函数?
2. 函数采用什么样的调用约定?
3. 函数的参数是怎样进行传递的?
4. 函数的返回值是通过怎样接收存储?
5. 栈帧是什么东西?
6. 栈帧有什么作用?
7. 栈帧在函数中的使用?
1. 怎样在ARM汇编中去识别和定位出函数
1.使用专门的跳转指令。
1.1 B 跳转指令
1.2 BL 带返回的跳转指令
1.3 BLX 带返回和状态切换的跳转指令
1.4 BX 带状态切换的跳转指令
2.直接向程序计数器 PC 写入跳转地址值。
通过向程序计数器 PC写入跳转地址值,可以实现在 4GB 的地址空间中的任意跳转,在跳转之前结合使用MOV LR,PC
总结:识别函数的方法就是汇编指令中是否有包含:B、BL、BLX、BX、PC的汇编指令。
2. 函数采用什么样的调用约定?
ARM函数调用约定采用的是:ATPCS
ATPCS的英文全称是ARM-THUMB procedure call standard(ARM-Thumb过程调用标准)
总结:参数1~参数4 分别保存到 R0~R3 寄存器中 ,剩下的参数从右往左一次入栈,被调用者实现栈平衡,返回值存放在 R0 中。
ATPCS主要设定的以下几个使用规则:
堆栈使用规则
ATPCS规定堆栈为FD类型,即满递减堆栈。并且堆栈的操作是8字节对齐。
而对于ARM汇编来说,如果目标文件中包含了外部调用,必须满足以下条件:
1.外部接口的数据栈一定是8位对齐的,也就是要保证在进入该汇编代码后,直到该汇编程序调用外部代码之间,数据栈的栈指针变化为偶数个字;
2.在汇编程序中使用PRESERVE8伪操作告诉连接器,本汇编程序是8字节对齐的.。
参数传递规则
根据参数个数是否固定,可以将子程序分为参数个数固定的子程序和参数个数可变的子程序.这两种子程序的参数传递规则是不同的.
1.参数个数可变的子程序参数传递规则
对于参数个数可变的子程序,当参数不超过4个时,可以使用寄存器R0~R3来进行参数传递,当参数超过4个时,还可以使用数据栈来传递参数. 在参数传递时,将所有参数看做是存放在连续的内存单元中的字数据。然后,依次将各名字数据传送到寄存器R0,R1,R2,R3;如果参数多于4个,将剩余的字数据传送到数据栈中,入栈的顺序与参数顺序相反,即最后一个字数据先入栈. 按照上面的规则,一个浮点数参数可以通过寄存器传递,也可以通过数据栈传递,也可能一半通过寄存器传递,另一半通过数据栈传递.
2.参数个数固定的子程序参数传递规则
对于参数个数固定的子程序,参数传递与参数个数可变的子程序参数传递规则不同,如果系统包含浮点运算的硬件部件。浮点参数将按照下面的规则传递:
(1)各个浮点参数按顺序处理;
(2)为每个浮点参数分配FP寄存器; 分配的方法是,满足该浮点参数需要的且编号最小的一组连续的FP寄存器.第一个整数参数通过寄存器R0~R3来传递,其他参数通过数据栈传递.
子程序结果返回规则
1.结果为一个32位的整数时,可以通过寄存器R0返回.
2.结果为一个64位整数时,可以通过R0和R1返回,依此类推.
3.结果为一个浮点数时,可以通过浮点运算部的寄存器f0,d0或者s0来返回.
4.结果为一个复合的浮点数时,可以通过寄存器f0-fn或者d0~dn来返回.
5.对于位数更多的结果,需要通过调用内存来传递.
总结:
r0–r3:存储传递给函数的参数值,多余的参数通过压栈传递。
r4 -r11:存储函数的局部变量,Thumb模式不会使用r8以后的寄存器
r12:是内部过程调用暂时寄存器(intra-procedure-call scratch register)。
r13:存储栈指针(sp)。在计算机中,栈非常重要。这个寄存器保存着栈顶的指针。这里可以看到更多关于栈的信息。
r14:链接寄存器(link register)。存储着当被调用函数返回时,将要执行的下一条指令的地址。
r15:用作程序计数器(program counter)。存储着当前执行指令的地址。每条执行被执行后,该计数器会进行自增(+1)。
函数的返回值放到r0中。
fp叫做frame pointer寄存器,即栈帧指针寄存器;sp叫做stack pointer寄存器,即栈指针寄存器。
在ARM指令系统中是地址递减栈,入栈操作的参数入栈顺序是从右到左依次入栈,而参数的出栈顺序则是从左到右的你操作。包括push/pop和LDMFD/STMFD等。
3.函数的参数是怎样进行传递的?
当参数个数小于等于4个的时候,使用r0到r3这4个寄存器进行参数传递;如果参数个数大于4个,余下的参数就通过sp所指向的数据栈进行参数传递。
比如有3个参数的话,那么r0代表函数的第一个参数,r1代表函数的第二个参数,r2代表函数的第三个参数。
比如有6个参数的话,那么r0-r3表示前面4个参数,然后余下的两个参数通过在栈上开辟8字节的空间进行参数传递。
看如下函数参数定义和调用实例:
参数小于四个的函数实现及ARM汇编实现
C++源代码:
看汇编前先熟记以下几个指令。
fp:栈底寄存器
sp:栈顶寄存器
ldr r0, [pc,#4] ; 把pc+4内存地址的值给r0
str r1, [pc,#4] ; 把r1的值写入pc+4内存地址
入栈指令:push、STM
Push {fp}
STMFD sp!{r4-r7,lr}
出栈指令:pop、LDM
Pop(fp)
LDMFD sp! {r4-r7,lr}
上图函数源码对应的ARM的汇编代码
以上汇编指令表示:
sub sp,sp,#28 开辟28栈空间
ADD指令表示用于相加操作:add r2, r2,r3 等于r2=r2+r3
BX表示带返回值的函数
以上代码中 bl 826C前面的三个ldr表示的是传递到bl826C的三个函数参数。
参数大于四个的情况
以上ARM汇编中在bx前面有再开辟完栈空间后有对应的六个ldr指令,表示这个函数有六个参数
4.函数的返回值是通过怎样接收存储?
1.结果为一个32位的整数时,可以通过寄存器R0返回.
2.结果为一个64位整数时,可以通过R0和R1返回,依此类推.
3.结果为一个浮点数时,可以通过浮点运算部件的寄存器f0,d0或者s0来返回.
4.结果为一个复合的浮点数时,可以通过寄存器f0-fN或者d0~dN来返回.
5.对于位数更多的结果,需要通过调用内存来传递.
先简单回归下函数中参数和返回值用的指令。
int armTest(int a, int b, int c, int d),
输入的参数:r0 = a, r1 = b, r2= c, r3 = d.
函数的返回:r0 = 类型为int的返回值。
int *armTest(char a, double b, int c, char d),
输入参数:r0 = a, r1用于对齐(double 要求8字节对齐), b = {r2, r3},c放在堆栈的sp[0]位置,d放在堆栈的sp[4]位置,这里的sp是指进入函数时的sp。
返回:r0 = 类型为int *的返回值。
函数调用完毕后,如果函数有返回值,函数一般把返回值保存在r0寄存器中,因此一般我们通过bl指令调用一个函数后,就可以通过在汇编里面访问r0得到返回值。
往下看分析下实例代码。
源代码情况
Main函数对应汇编
0x00010418<+0>: push{r11, lr}
0x0001041c<+4>: add r11,sp,#4
0x00010420<+8>: sub sp,sp,#8 为局部变量开辟空间
0x00010424<+12>: mov r3,#5 参数 5;r3=5
0x00010428<+16>: str r3, [sp]
0x0001042c<+20>: mov r3,#6 参数6 r3=6
0x00010430<+24>: str r3, [sp, #4]
0x00010434<+28>: mov r0, #1 参数1
0x00010438<+32>: mov r1, #2 参数2
0x0001043c<+36>: mov r2, #3 参数3
0x00010440<+40>: mov r3, #4 参数4
---->0x00010444<+44>:bl 0x103c8<func>
0x00010448<+48>: mov r3, #0 r3=0
0x0001044c<+52>: mov r0, r3 返回值r0=0
0x00010450<+56>: sub sp, r11, #4
0x00010454<+60>: pop {r11, pc}
Func函数对应的ARM汇编
0x000103c8 <+0>: push{r11} ; (str r11, [sp, #-4]!)
0x000103cc <+4>: add r11, sp, #0
0x000103d0 <+8>: sub sp, sp, #20 开辟栈空间#20
0x000103d4<+12>: str r0, [r11, #-8] r0写入到[r11, #-8]的位置
0x000103d8 <+16>: str r1, [r11, #-12]
0x000103dc <+20>: str r2, [r11, #-16]
0x000103e0 <+24>: str r3, [r11, #-20] ; 0xffffffec
0x000103e4 <+28>: ldr r2, [r11, #-8] 取出[r11,#-8]空间的值写入到r2寄存器中
0x000103e8 <+32>: ldr r3, [r11, #-12]
0x000103ec <+36>: add r2, r2, r3 r2=r2+r3
0x000103f0 <+40>: ldr r3, [r11, #-16]
0x000103f4 <+44>: add r2, r2, r3 r2=r2+r3
0x000103f8 <+48>: ldr r3, [r11, #-20] ; 0xffffffec
----> 0x000103fc <+52>:add r2,r2, r3
0x00010400 <+56>: ldr r3, [r11, #4]
0x00010404 <+60>: add r3, r2, r3 r3= r2+r3
0x00010408 <+64>: mov r0, r3 存储返回值r0= r3
0x0001040c <+68>: sub sp,r11, #0 恢复栈空间
0x00010410 <+72>: pop {r11} ; (ldr r11, [sp], #4)
0x00010414 <+76>: bx lr 函数结束跳转出来
5.栈帧是什么东西?
1.栈是一种具有后进先出的数据组织方式,也就是说后存放的先取出,先存放的后取出。栈底是第一个进栈的数据所处位置,栈顶是最后一个数据进栈所处的位置。
栈帧(stack frame):就是一个函数所使用的那部分栈,所有函数的栈帧串起来就组成了一个完整的栈。栈帧的两个边界分别由fp(r11)和sp(r13)来限定。
栈帧是如何形成的呢? 当栈顶指针 sp小于栈底指针 fp时, 就形成了栈帧。
下图是ARM的栈帧布局
上图描述的是ARM的栈帧布局方式,main stack frame为调用函数的栈帧,func1stack frame为当前函数(被调用者)的栈帧,栈底在高地址,栈向下增长。图中FP就是栈基址,它指向函数的栈帧起始地址;SP则是函数的栈指针,它指向栈顶的位置。ARM压栈的顺序依次为当前函数指针PC、返回指针LR、栈指针SP、栈基址FP、传入参数个数及指针、本地变量和临时变量。如果函数准备调用另一个函数,跳转之前临时变量区先要保存另一个函数的参数。
函数调用过程中涉及四个重要的寄存器:PC、LR、SP和FP。注意,每个栈帧中的PC、LR、SP和FP都是寄存器的历史值,并非当前值。PC寄存器和LR寄存器均指向代码段, 其中PC代表代码当前执行到哪里了,LR代表当前函数返回后,要回到哪里去继续执行。SP和FP用来维护栈空间,其中SP指向栈顶,FP指向上一个栈帧的栈顶。
因此得出:栈是通过FP和SP寄存器串成一串的,每个单元就是一个栈帧(也就是一个函数调用过程)。又由于LR是指向调用函数的(即PC寄存器的历史值)。那么,如果能得到每个栈帧中的LR值,就能得到整个的函数调用链。
6.栈帧的作用?
1、返回地址
函数完成后要返回的程序内部地址
2、局部数据存储
为局部变量分配的内存
3、参数存储
为函数参数分配的内存
4、栈指针和基指针
运行时系统用来管理栈的指针
栈指针通常指向栈的顶部,基指针(帧指针)通常存在并指向栈帧内部的地址。
下例中栈就是用于存储保存局部变量的。
栈用于存储返回值的
Str r0,[r7,#12]意思是将返回值R0写入到栈[r7,#12]的位置
7. 栈帧在函数中的使用?
ARM压栈的顺序很是规矩,依次为当前函数指针PC、返回指针LR、栈指针SP、栈基址FP、传入参数个数及指针、本地变量和临时变量。
ARM进行函数内压栈和出栈往往使用如下的语句:
stmfd sp!,{r0-r9, lr} ; 满递减入栈,给寄存器r0-r9,lr压栈,sp不断减4。
ldmfd sp!,{r0-r9, pc} ; 满递减出栈,给寄存器r0-r9出栈,并使程序跳转回函数的调用点,sp不断增4。
最后总结:
函数指令:B、BL、BX、BLX、PC
函数调用约定:ATPCS
函数参数:小于等于4个:R0-R3; 大于4个用SP栈表示。
函数返回值:R0
栈可用于:局部变量,返回值,参数传递
函数传递及调用常用指令
fp:栈底寄存器
sp:栈顶寄存器
ldr r0, [pc,#4] ; 把pc+4内存地址的值给r0
str r1, [pc,#4] ; 把r1的值写入pc+4内存地址
push,pop
lr:链接地址寄存器,存放下一条执行指令